#!/usr/bin/env python 
import time
import re

class DateImpl(object):
	"""Internal rappresentation of a date, starts at given time from epoch or current time"""
	def __init__(self, tval=None):
		if tval == None:
			tval = time.time()
		self.val = tval
	def as_tuple(self):
		return time.localtime(self.val)
	def from_tuple(self, date):
		self.val = time.mktime(date)
		return self
	def __getattr__(self, attr):
		if attr == 'year':
			return self.from_tuple()[0]
		elif attr == 'month':
			return self.from_tuple()[1]
		elif attr == 'day':
			return self.from_tuple()[2]
		elif attr == 'hour':
			return self.from_tuple()[3]
		elif attr == 'minute':
			return self.from_tuple()[4]
		elif attr == 'second':
			return self.from_tuple()[5]
		else:
			raise AttributeError, attr
			
class Date(object):
	default_format = '%Y/%m/%d'
	def __init__(self, cdate=None, format=None):
		"""Creates a new Date from date. cdate must be a string respecting the format specified or 
		the format. If no cdate is specified then the current date is used"""
		self.add_re = re.compile(r'^([+-]?\d+d)?([+-]?\d+m)?([+-]?\d+y)?([+-]?\d+H)?([+-]?\d+M)?([+-]?\d+S)?$')
		self.unit_re = re.compile(r'^(d|m|y|H|M|S)$')
		
		self.format = format or Date.default_format
		self.unit = 'S'
		
		if cdate and type(cdate) == type(''):
			cdate = time.strptime(cdate, self.format)
			cdate = time.mktime(cdate)
			
		self._impl = DateImpl(cdate)

	def __setattr__(self, attr, val):
		"""it makes possible to set .default_unit e .format checking on correctness of .default_unit
		default_unit should be one of d|m|y|H|M|S
		format uses the rules valid for format parameter of as_string method"""
		if attr == 'default_unit':
			if type(val) == type('') and self.unit_re.match(val):
				object.__setattr__(self, 'default_unit', val)
			else:
				raise ValueError,"default_unit must be one of: d,m,y,H,M,S"
		else:
			object.__setattr__(self, attr, val)
			
	def __str__(self):
		"""Returns default string rappresentation of the date based on the .format propierty"""
		return self.as_string(self.format)

	def as_string(self, format):
		"""Converts the date to a formatted string. Format should be in the form:
		%a Locale's abbreviated weekday name.
		%A Locale's full weekday name.
		%b Locale's abbreviated month name.
		%B Locale's full month name.
		%c Locale's appropriate date and time representation.
		%d Day of the month as a decimal number [01,31].
		%H Hour (24-hour clock) as a decimal number [00,23].
		%I Hour (12-hour clock) as a decimal number [01,12].
		%j Day of the year as a decimal number [001,366].
		%m Month as a decimal number [01,12].
		%M Minute as a decimal number [00,59].
		%p Locale's equivalent of either AM or PM.
		%S Second as a decimal number [00,61].
		%U Week number of the year (Sunday as the first day)
		%w Weekday as a decimal number [0(Sunday),6].
		%W Week number of the year (Monday as the first day of the week)
		%x Locale's appropriate date representation.
		%X Locale's appropriate time representation.
		%y Year without century as a decimal number [00,99].
		%Y Year with century as a decimal number.
		%Z Time zone name (no characters if no time zone exists).
		%% A literal "%" character."""
		return time.strftime(format, self._impl.as_tuple())
		
	def as_tuple(self):
		"""Converts the date to a tuple of the form [y,d,m,H,M,S,working_week_day,day_of_the_year,dst(ora_legale/ora_solare)]"""
		return self._impl.as_tuple()

	def __eq__(self, other):
		"""Compares a date to another one or to a string in a valid form from which can be created a date"""
		if type(self) == type(other):
			return self.as_string(Date.default_format) == other.as_string(Date.default_format)
		elif type(other) == type(''):
			return self == Date(other)
		elif other == None:
			return False
		else:
			raise Exception, "Unknown comparison with date object"

	def __add__(self, value):
		"""Adds value to date. If value is a number it is added to the default_unit, 
		otherwise it should be a part of the string 'NdNmNyNHNMNS' 
		where N is a number and d,m,y,H,M,S means days,months,years,hours,minutes,seconds"""
		ris = []
		ris.extend(self._impl.as_tuple())
		ris[6] = ris[7] = ris[8] = -1
		try: 
			int(value)
			value = str(value) + self.default_unit
		except:	pass
		match = self.add_re.match(value.strip())
		if match:
			for i in range(1,7):
				ctag = match.group(i)
				if ctag == None:
					continue
			
				val = ctag[0:-1]
				if ctag[-1] == 'y':
					ris[0]+=int(val)
				elif ctag[-1] == 'm':
					ris[1]+=int(val)
				elif ctag[-1] == 'd':
					ris[2]+=int(val)
				elif ctag[-1] == 'H':
					ris[3]+=int(val)
				elif ctag[-1] == 'M':
					ris[4]+=int(val)
				elif ctag[-1] == 'S':
					ris[5]+=int(val)
		return Date(DateImpl().from_tuple(ris).val)
		
	def __sub__(self, value):
		"""Substracts value to Date, using the same constraint on value as __add__"""
		try: 
			value = -int(value)
		except:
			match = self.add_re.match(value.strip())
			value = ''
			if match:
				for i in range(1,7):
					ctag = match.group(i)
					if ctag == None:
						continue
					elif ctag[0] == '+':
						value += '-' + ctag[1:]
					elif ctag[0] == '-':
						value += '+' + ctag[1:]
					else:
						value += '-' + ctag
		return self + value
		
	def __lt__(self, value):
		if type(value) == type(''):
			return self < Date(value)
		else:
			rd1 = self.as_string(Date.default_format)
			rd2 = value.as_string(Date.default_format)
			rd1 = Date(rd1)
			rd2 = Date(rd2)
			return rd1._impl.val < rd2._impl.val
			
	def __le__(self, value):
		return ((self < value) or (self == value))
		
	def __ge__(self, value):
		return not (self < value)
		
	def __gt__(self, value):
		return not (self <= value)
		
	def __ne__(self, value):
		return not (self == value)
			
	def isValid(date):
		"""Checks if a string rappresents a valid date based on the default_format
		If the date is valid it returns the corrispective Date, else None"""
		try: return Date(date)
		except:	return None
	isValid = staticmethod(isValid)

if __name__ == '__main__':
	import unittest as pyu
	class GeneralFixture(pyu.TestCase):
		def setUp(self):
			Date.default_format = '%d/%m/%Y %H:%M:%S'
			self.d1 = Date()
			self.d2 = Date()
				
		def testEquality(self):
			"""Dates with same content should result equal"""
			self.d1._impl.val = self.d2._impl.val = time.time()
			self.assertEqual(self.d1, self.d2)
			
		def testStringEquality(self):
			"""Dates should be comparable with string rappresentation"""
			self.d1 = Date("11/5/2002 0:0:0")
			self.assertEqual(self.d1, "11/05/2002 0:0:0")
			
		def testStringSimpleEquality(self):
			"""Dates should be comparable to strings in only one part"""
			olddformat = Date.default_format
			self.d1 = Date("11/5/2002 15:34:21")
			Date.default_format = "%Y-%m-%d"
			self.assertEqual(self.d1, "2002-5-11")
			Date.default_format = olddformat
			
		def testDifference(self):
			"""Dates should be compared basing on format"""
			self.d1._impl.val = time.time()
			self.d2._impl.val = time.time()+1 #Un secondo di differenza
			self.assertNotEqual(self.d1, self.d2)
			oldformat,Date.default_format = Date.default_format,'%d/%m/%y %H:%M'
			self.assertEqual(self.d1, self.d2)
			Date.default_format = oldformat
			
			
		def testStringInizialization(self):
			"""Dates with same string initialization but different creation format should be equal"""
			self.d1 = Date("1/1/2003 0:0:0")
			self.d2 = Date("01/01/03 00:00:00", '%d/%m/%y %H:%M:%S')
			self.assertEqual(self.d1, self.d2)
			
		def testDayConstraint(self):
			"""Simple check on days: They are from 1-39, there should not be a day >39 or <1"""
			self.assertRaises(Exception, Date.__init__, "40/12/82 0:0:0")
			self.assertRaises(Exception, Date.__init__, "0/12/82 0:0:0")
			
		def testMonthConstraint(self):
			"""Months should be between 1-12"""
			self.assertRaises(Exception, Date.__init__, "11/13/82 0:0:0")
			self.assertRaises(Exception, Date.__init__, "11/0/82 0:0:0")
			
		def testYearConstraint(self):
			"""Years should have at least 2 numbers and no more than 4"""
			self.assertRaises(Exception, Date.__init__, "11/11/0 0:0:0")
			self.assertRaises(Exception, Date.__init__, "11/11/20005 0:0:0")
			
		def testDefaultUnit(self):
			"""default_unit can be only one of y,d,m,H,M,S or should throw a ValueError"""
			self.assertRaises(ValueError, self.d1.__setattr__, 'default_unit', 'j')
			self.assertRaises(ValueError, self.d1.__setattr__, 'default_unit', 'K')
			self.assertRaises(ValueError, self.d1.__setattr__, 'default_unit', 'L')
			self.assertRaises(ValueError, self.d1.__setattr__, 'default_unit', 'P')
			self.d1.default_unit = 'H'
			self.d1.default_unit = 'M'
			self.d1.default_unit = 'S'
			self.d1.default_unit = 'd'
			self.d1.default_unit = 'y'
			self.d1.default_unit = 'm'
			
		def testValidity(self):
			"""Date.isValid should check if a string is a valid date or not"""
			oldformat,Date.default_format = Date.default_format,'%d-%m-%Y'
			self.assertNotEqual(Date.isValid("11-10-2002"), None)
			self.assertEqual(Date.isValid("11/10/02"), None)
			self.assertEqual(Date.isValid("11-10-02"), None)
			Date.default_format = oldformat
			
	class ConversionFixture(pyu.TestCase):
		def setUp(self):
			Date.default_format = '%d/%m/%Y %H:%M:%S'
			self.d1 = Date()
	
		def testFromTuple(self):
			"""From tuple should return a tuple with 9 int arguments: (y,m,d,h,m,s,wd,yd,dst)"""
			self.d1 = Date("13/11/2002 15:31:47")
			t = self.d1.as_tuple()
			self.assertEqual(t[0], 2002)
			self.assertEqual(t[1], 11)
			self.assertEqual(t[2], 13)
			self.assertEqual(t[3], 15)
			self.assertEqual(t[4], 31)
			self.assertEqual(t[5], 47)
			
		def testFromStringDefault(self):
			"""From string should use default format conversion"""
			self.d1 = Date("14/10/82 0:0:42", '%d/%m/%y %H:%M:%S')
			self.d1.format = '%d %d %m %S %M %y %H'
			self.assertEqual(str(self.d1),'14 14 10 42 00 82 00')
			
		def testFromString(self):
			"""From string should return converted string or ignored tags"""
			self.d1 = Date("14/10/82 0:0:42", '%d/%m/%y %H:%M:%S')
			self.assertEqual(self.d1.as_string("%J%H"), '%J00')
	
	class OperationsFixture(pyu.TestCase):
		def setUp(self):
			Date.default_format = '%d/%m/%Y %H:%M:%S'
			self.d = Date()
			
		def testDefaultUnitAdding(self):
			"""operator + should add to default_unit by default"""
			oldd = self.d
			self.d.default_unit='d'
			self.d = self.d + 5
			self.assertEqual(self.d.as_tuple()[2], oldd.as_tuple()[2]+5)
			
		def testStringAddition(self):
			"""operator + should add correctly to relative string specification of the form \d[d|m|y|H|M|S]"""
			self.d = Date("1/1/2001 0:0:0")
		
			self.d += '5y'
			self.assertEqual(self.d.as_tuple()[0], 2006)
			
			self.d += '5m'
			self.assertEqual(self.d.as_tuple()[1], 6)
			
			self.d += '5d'
			self.assertEqual(self.d.as_tuple()[2], 6)

			self.d += '5H'
			self.assertEqual(self.d.as_tuple()[3], 5)
			
			self.d += '5M'
			self.assertEqual(self.d.as_tuple()[4], 5)
			
			self.d += '5S'
			self.assertEqual(self.d.as_tuple()[5], 5)
			
		def testStringAdditionConstraint(self):
			"""operator + should change the date with a badly formatted string"""
			self.d = Date("1/1/2002 0:0:0")
			self.d += '6j'
			self.assertEqual(self.d, "1/1/2002 0:0:0")
			
		def testStringAdditionComplex(self):
			"""operator + should parse addition string to add every specified element"""
			self.d = Date("1/1/2002 0:0:0")
			self.d += '2d2m2y2H2M2S'
			self.assertEqual(self.d, "3/3/2004 2:2:2")
			
		def testNegativeAddition(self):
			"""operator + should be able to add negative values"""
			self.d = Date("5/5/2005 5:5:5")
			self.d.default_unit = 'y'
			self.d += -2
			self.assertEqual(self.d, "5/5/2003 5:5:5")
	
		def testSubstraction(self):
			"""operator - should work as operator + but substracting values instead of adding them"""
			self.d = Date("5/5/2005 5:5:5")
			self.d.default_unit = 'y'
			self.d -= 2
			self.assertEqual(self.d, "5/5/2003 5:5:5")
			
		def testStringSubstraction(self):
			"""operator - should substract even in string form"""
			self.d = Date("5/5/2005 5:5:5")
			self.d -= '2y'
			self.assertEqual(self.d, "5/5/2003 5:5:5")
			self.d -= '-2y'
			self.assertEqual(self.d, "5/5/2005 5:5:5")
			
		def testMixedSubstraction(self):
			"""operator - should invert every element in string"""
			self.d = Date("5/5/2005 5:5:5")
			self.d -= '4d+4m-5y'
			self.assertEqual(self.d, '1/1/2010 5:5:5')
			
		def testLessThen(self):
			"""< and <= should compare basing on default_format"""
			oldformat,Date.default_format = Date.default_format,'%d/%m/%Y'
			d1 = Date("4/5/2005")
			d2 = Date("5/5/2005")
			
			self.assertEqual(d1.__lt__(d2), True)
			
			d1 = Date("5/5/2005")
			self.assertEqual(d1.__lt__(d2), False)
			
			d1 = Date('5/3/2005')
			Date.default_format = '%d/%Y'
			self.assertEqual(d1.__lt__(d2), False)
			Date.default_format = '%m/%Y'
			self.assertEqual(d1.__lt__(d2), True)
			Date.default_format = '%d/%Y'
			self.assertEqual(d1<=d2, True)
			Date.default_format = '%d/%m/%y'
			self.assertEqual(d1<'6/3/05', True)
			
			Date.default_format = oldformat
			
		def testMoreThen(self):
			"""> and >= should compare basing on default_format as < and <="""
			oldformat,Date.default_format = Date.default_format,'%d/%m/%Y'
			d1 = Date("4/5/2005")
			d2 = Date("5/5/2005")
			
			self.assertEqual(d2 > d1, True)
			
			d1 = Date("5/5/2005")
			self.assertEqual(d2 > d1, False)
			
			d1 = Date('5/3/2005')
			Date.default_format = '%d/%Y'
			self.assertEqual(d2 > d1, False)
			Date.default_format = '%m/%Y'
			self.assertEqual(d2 > d1, True)
			Date.default_format = '%d/%Y'
			self.assertEqual(d2 >= d1, True)
			Date.default_format = '%d/%m/%y'
			self.assertEqual(d2 >= '4/5/03', True)
			
			Date.default_format = oldformat
	
	pyu.main(testRunner=pyu.TextTestRunner(verbosity=5))
	